--[[
===== Syntax Definition =====
[varname], square-brackets, variable
"fixedstring", double-quotes, a fixed string value
..., ellipsis, repeat 0 to N times


===== Data Structure =====
ConfigItem = {
	Key = "[keyname]",
	Type = "[type]",
	Value = "[value]",
	Condition = "[cond]" | nil,
	Target = "[unit]" | nil,
	TargetFrames = "[framname1], [framname2], ..." | nil,
	Comment = "[comment]" | nil,
}

UserItem = {
	Key = "[keyname]",
	Type = "[type]",
	Value = "[value]",
	Condition = "[cond]" | "nil",
	Target = "[unit]" | nil,
	TargetFrames = {1 = "[framname1]", 2 = "[framname2]", ...},
	Modifier = "[mod]-" | "",
	Button = [buttonnum] | nil
}

Config = {
	1 = {[ConfigItem1], [ConfigItem2], ...},
	2 = {[ConfigItem1], [ConfigItem2], ...},
	...
}

SelectedConfigGroup = {
	1 = [groupindex1],
	2 = [groupindex2],
	...
}

UserConfig.Items = {
	1 = [UserItem1],
	2 = [UserItem2],
	...
}

InnerConfig.Items = {
	[targetframenameN] = {
		ConditionSecquence = {1 = "[condition1]", 2 = "[condition2]", ...},

		[condition1] = {
			[keyname1] = [UserItem1],
			[keyname2] = [UserItem2],
			...
		},

		[condition2] = {
			[keyname1] = [UserItem1],
			[keyname2] = [UserItem2],
			...
		},

		...
	},
}

Header.Attributes = { button[i] = "[buttonname]", ... }

Button.Attributes = {
	conds = "[conditions]" | nil,
	bakunit = "[original unit]" | nil,
	laststate = "[laststatevalue]" | nil,
	cond[i]-[count]-[varname] = "[varvalue]", ...
}

]]

ShortKey = {
	Config = {},
	SelectedConfigGroup = {},
	IsAutoLoad = true,
	IsAutoSelect = true,
}

local MouseButtons = {
	LeftButton = 1,
	RightButton = 2,
	MiddleButton = 3,
	Button4 = 4,
	Button5 = 5,
	Button6 = 6,
	Button7 = 7,
	Button8 = 8,
	Button9 = 9,
	Button10 = 10,
	Button11 = 11,
	Button12 = 12,
	Button13 = 13,
	Button14 = 14,
	Button15 = 15,
}

local Modifiers = {
	CTRL = true,
	SHIFT = true,
	ALT = true,
}

local LoadState = false;
local NILCONDITION = "nil"

local ShortKeyHeader = {
	Item,
}

local ShortKeyButton = {
	Items = {},
}

local UserConfig = {
	Items = {},
}

local InnerConfig = {
	Items = {},
}

ShortKey.ShortKeyHeader = ShortKeyHeader
ShortKey.ShortKeyButton = ShortKeyButton
ShortKey.UserConfig = UserConfig
ShortKey.InnerConfig = InnerConfig

------------------------------------------------------------------------------------------------------
--local functions
------------------------------------------------------------------------------------------------------

-- nil, empty, not string type will return true
local function IsStringEmpty(text)
	if (text == nil or text == "" or type(text) ~= "string") then
		return true
	end
	return false
end

--return ismousebutton, modifier, mousebutton
local function IsMouseButton(key)
	local modifier, dash, button = string.match(key, "(%a*)(-?)(.*Button.?)")

	if (dash ~= "-") then
		dash = nil
	end

	if (not dash and button == "Button") then
		button = key
	end

	if (not Modifiers[modifier]) then
		modifier = nil
	end

	if (not MouseButtons[button]) then
		button = nil
	end

	if (not modifier and not dash and button) then
		return true, "", MouseButtons[button]
	elseif (modifier and dash and button) then
		return true, string.lower(modifier).."-", MouseButtons[button]
	end
end

local function GetButtonObject(buttonname)
	local button = getglobal(buttonname)

	if (button) then
		local objtype = button:GetObjectType()
		if (objtype == "Button" or objtype == "CheckButton") then
			return button
		end
	end
end

local function ConcatConfigItem(configitem)
	local targetframes = configitem.TargetFrames

	if (configitem.TargetFrames and type(configitem.TargetFrames) == "table") then
		targetframes = table.concat(configitem.TargetFrames, ",")
	end

	return string.format("Key:%s Type:%s Value:%s Target:%s Condition:%s TargetFrames:%s",
				configitem.Key  or "nil",
				configitem.Type or "nil",
				configitem.Value or "nil",
				configitem.Target or "nil",
				configitem.Condition or "nil",
				targetframes or "nil")
end

local function ErrorReport(innerconfigitem, configitem, msg)
	print("ShortKey config error: "..msg)

	if (innerconfigitem) then
		print(string.format("src item: [%s]", ConcatConfigItem(innerconfigitem)))
	end

	if (configitem) then
		print(string.format("dst item: [%s]", ConcatConfigItem(configitem)))
	end
end

local function debug(frame, ...)
	for i = 1, select("#", ...) do
		local val = select(i, ...) or "nil"
		print (i.."    "..val)
	end
	print("---------------------------")
end

------------------------------------------------------------------------------------------------------
--ShortKey
------------------------------------------------------------------------------------------------------
function ShortKey.Init()
	BINDING_HEADER_ShortKeyTitle = "ShortKey By simonw. http://cwowaddon.com"
	BINDING_NAME_ShortKey1 = "Reload UI"
	--BINDING_NAME_ShortKey2 = "List Binding Key"
	print("ShortKey installed. By simonw from CWDG cwowaddon.com")

	ShortKey.LoadSetting()

	if (ShortKey.IsAutoLoad) then
		ShortKey.Load()
	else
		print("ShortKey is set to load by manual, you need to use command [/sk load] to load it.")
	end

	ShortKey.ShowState()
end

function ShortKey.LoadSetting()
	if (ShortKeySetting) then
		if (ShortKeySetting.IsAutoLoad ~= nil) then
			ShortKey.IsAutoLoad = ShortKeySetting.IsAutoLoad
		end

		if (ShortKeySetting.IsAutoSelect ~= nil) then
			ShortKey.IsAutoSelect = ShortKeySetting.IsAutoSelect
		end

		if (ShortKeySetting.SelectedConfigGroup ~= nil) then
			ShortKey.SelectedConfigGroup = ShortKeySetting.SelectedConfigGroup
		end
	end
end

function ShortKey.SaveSetting()
	ShortKeySetting = {}
	ShortKeySetting.IsAutoLoad = ShortKey.IsAutoLoad
	ShortKeySetting.IsAutoSelect = ShortKey.IsAutoSelect
	ShortKeySetting.SelectedConfigGroup = ShortKey.SelectedConfigGroup
end

function ShortKey.ClearSetting()
	ShortKeySetting = nil
	print("ShortKey setting is cleared, you need to reload UI.")
end

function ShortKey.Load()
	if (not LoadState) then
		LoadState = true

		if (ShortKey.IsAutoSelect) then
			ShortKey.SelectedConfigGroup = UserConfig:LoadAuto(ShortKey.Config)
		else
			UserConfig:LoadManual(ShortKey.Config, ShortKey.SelectedConfigGroup)
		end

		InnerConfig:Import(UserConfig)

		--non secure environment
		local header = ShortKeyHeader:Create()
		InnerConfig:FillHeader(header)
		--secure environment
		ShortKeyHeader:Execute()
	else
		ErrorReport(nil, nil, "ShortKey has been already loaded, you can't load it again until reload UI.")
	end
end

function ShortKey.ShowState()
	local info = {}
	info["AutoLoad"] = tostring(ShortKey.IsAutoLoad)
	info["AutoSelect"] = tostring(ShortKey.IsAutoSelect)
	info["AddonLoaded"] = tostring(LoadState)
	if (LoadState) then
		info["LoadedGroup"] = table.concat(ShortKey.SelectedConfigGroup, ",")
	else
		info["LoadedGroup"] = "none"
	end

	local msg = "ShortKey Info: "

	for k, v in pairs(info) do
		msg = msg..k..":"..v.." "
	end

	print(msg)
end

function ShortKey.ShowList()
	for _, useritem in ipairs(UserConfig.Items) do
		print(ConcatConfigItem(useritem))
	end
end

function ShortKey.Setting(context)
	if (context and context.Slash_Arg) then
		local cmd = string.lower(context.Slash_Args[1] or "")

		if (cmd == "autoload") then
			ShortKey.IsAutoLoad = not ShortKey.IsAutoLoad
			ShortKey.SaveSetting()
			print("AutoLoad:"..tostring(ShortKey.IsAutoLoad))

		elseif (cmd == "autoselect") then
			--ShortKey.IsAutoSelect = not ShortKey.IsAutoSelect
			--ShortKey.SaveSetting()

		elseif (cmd == "clear") then
			ShortKey.ClearSetting()

		elseif (cmd == "list") then
			ShortKey.ShowList()

		elseif (cmd == "load") then
			ShortKey.Load()
		end
	else
		ShortKey.ShowState()
	end
end

------------------------------------------------------------------------------------------------------
--ShortKeyButton
------------------------------------------------------------------------------------------------------

function ShortKeyButton:Create()
	local index = table.getn(self.Items) + 1
	local buttonname = "ShortKeyActionButton"..index
	CreateFrame("Button", buttonname, UIParent, "SecureActionButtonTemplate")
	table.insert(self.Items, buttonname)
	return buttonname
end

------------------------------------------------------------------------------------------------------
--ShortKeyHeader
------------------------------------------------------------------------------------------------------

function ShortKeyHeader:Create()
	self.Item = CreateFrame("Frame", "ShortKeyHeaderFrame", nil, "SecureHandlerStateTemplate");
	self.Item:SetAttribute("BindButton", self.BindButton)
	self.Item:SetAttribute("UnBindButton", self.UnBindButton)

	--non secure environment
	self.Item.AddWatch = function (header, buttonname, conds)
		header:SetAttribute("_onstate-"..buttonname, [[control:RunAttribute("BindButton", stateid, newstate)]])
		RegisterStateDriver(header, buttonname, conds)
	end

	self.Item.debug = debug
	return self.Item
end

function ShortKeyHeader:Execute()
	self.Item:Execute(self.Bind)
end

ShortKeyHeader.Bind = [[
	TypeValueTable = table.new()
	TypeValueTable["actionbar"] = "action"
	TypeValueTable["pet"] = "action"
	TypeValueTable["maintank"] = "action"
	TypeValueTable["mainassist"] = "action"
	TypeValueTable["click"] = "clickbutton"
	TypeValueTable["macro"] = "macrotext"

	local index = 0

	while (true) do
		index = index + 1
		local buttonname = self:GetAttribute("button"..index)

		if (not buttonname) then
			return
		end

		local button = self:GetFrameRef(buttonname)
		local conds = button:GetAttribute("conds")

		if (conds) then
			control:CallMethod("AddWatch", buttonname, conds)
		else
			control:RunAttribute("BindButton", buttonname, 1)
		end
	end
]]

ShortKeyHeader.BindButton = [[--arguments: buttonname, condid
	local buttonname, condid = ...
	local button = self:GetFrameRef(buttonname)

	local perperfix = "cond"..condid.."-"
	local perfix
	local count = 0

	--first clean last bindings then binding new
	local laststate = button:GetAttribute("laststate")
	if (laststate ~= nil and laststate ~= condid) then
		control:RunAttribute("UnBindButton", buttonname, laststate)
	end
	--set newstate
	button:SetAttribute("laststate", condid)

	local itemkey, itemmodifier, itembutton, itemtype, itemvalue, itemtarget, itemtypevalue

	while (true) do
		count = count + 1
		perfix = perperfix..count.."-"

		itemkey = button:GetAttribute(perfix.."key")

		--jump while loop condition
		if (not itemkey) then
			return
		end

		itemmodifier = button:GetAttribute(perfix.."modifier")
		itembutton = button:GetAttribute(perfix.."button")
		itemtype = button:GetAttribute(perfix.."type")
		itemvalue = button:GetAttribute(perfix.."value")
		itemtarget = button:GetAttribute(perfix.."target")
		itemtypevalue = TypeValueTable[itemtype] or itemtype

		--control:CallMethod("debug", button:GetName(),itemkey,itemmodifier,itembutton,itemtype,itemvalue,itemtarget,itemtypevalue)

		if (itemtarget) then
			button:SetAttribute("unit", itemtarget)
		else
			button:SetAttribute("unit", button:GetAttribute("bakunit"))
		end

		if (itembutton) then
			button:SetAttribute(itemmodifier.."type"..itembutton, itemtype)
			button:SetAttribute(itemmodifier..itemtypevalue..itembutton, itemvalue)
		else
			if (itemtype == "command") then
				button:SetBinding(true, itemkey, itemvalue)
			else
				button:SetAttribute("type", itemtype)
				button:SetAttribute(itemtypevalue, itemvalue)
				button:SetBindingClick(true, itemkey, button:GetName())
			end
		end
	end
]]

ShortKeyHeader.UnBindButton = [[--arguments: buttonname, condid
	local buttonname, condid = ...
	local button = self:GetFrameRef(buttonname)

	local perperfix = "cond"..condid.."-"
	local perfix
	local count = 0

	local itemkey, itemmodifier, itembutton, itemtype, itemtypevalue

	while (true) do
		count = count + 1
		perfix = perperfix..count.."-"

		itemkey = button:GetAttribute(perfix.."key")

		if (not itemkey) then
			return
		end

		itemmodifier = button:GetAttribute(perfix.."modifier")
		itembutton = button:GetAttribute(perfix.."button")
		itemtype = button:GetAttribute(perfix.."type")
		itemtypevalue = TypeValueTable[itemtype] or itemtype

		if (itembutton) then
			button:SetAttribute(itemmodifier.."type"..itembutton, nil)
			button:SetAttribute(itemmodifier..itemtypevalue..itembutton, nil)
		else
			button:SetAttribute("type", nil)
			button:SetAttribute(itemtypevalue, nil)
			button:ClearBinding(itemkey)
		end
	end
]]

------------------------------------------------------------------------------------------------------
--UserConfig
------------------------------------------------------------------------------------------------------

function UserConfig:IsGroupLoad(group)
	if (not IsStringEmpty(group.Class) and group.Class ~= UnitClass("player")) then
		return false
	elseif (not IsStringEmpty(group.Character) and group.Character ~= UnitName("player")) then
		return false
	elseif (not IsStringEmpty(group.Race) and group.Race ~= UnitRace("player")) then
		return false
	elseif (group.Talent and tonumber(group.Talent) ~= GetActiveTalentGroup()) then
		return false
	end

	return true
end

function UserConfig:LoadManual(configs, selectedgroup)
	for _, index in ipairs(selectedgroup) do
		for _, useritem in ipairs(configs[index]) do
			table.insert(self.Items, useritem)
		end
	end
end

function UserConfig:LoadAuto(configs)
	local isload = false
	local selectedgroup = {}

	for index, group in ipairs(configs) do
		if (not group.IsLoad) then
			isload = self:IsGroupLoad(group)
		else
			isload = group:IsLoad()
		end

		if (isload) then
			table.insert(selectedgroup, index)

			for _, useritem in ipairs(group) do
				table.insert(self.Items, useritem)
			end
		end
	end

	return selectedgroup
end

function UserConfig:ItemInit(useritem)
	--check Key exist, must have it
	if (IsStringEmpty(useritem.Key)) then
		ErrorReport(nil, useritem, "Key not exist")
		return false
	end

	--init to set mouse button
	local ismouse, modifier, button = IsMouseButton(useritem.Key)
	useritem.Modifier = modifier
	useritem.Button = button

	--init to parse TargetFrames to table type
	if (not IsStringEmpty(useritem.TargetFrames)) then
		local frames = {}
		local framename

		for i = 1, select("#", strsplit(",", useritem.TargetFrames)) do
			framename = select(i,strsplit(",", useritem.TargetFrames))
			table.insert(frames, strtrim(framename));
		end

		useritem.TargetFrames = frames
	else
		useritem.TargetFrames = nil
	end

	--check mouse key must be pointed a TargetFrame
	if (useritem.TargetFrames == nil and ismouse) then
		ErrorReport(nil, useritem, "TargetFrames not exist when Key is mouse button")
		return false
	end

	--init to set default value to condition if empty
	if (IsStringEmpty(useritem.Condition)) then
		useritem.Condition = NILCONDITION
	end

	return true
end

------------------------------------------------------------------------------------------------------
--InnerConfig
------------------------------------------------------------------------------------------------------

function InnerConfig:ItemInit(useritem)
	--check mouse key conflict
	if (useritem.Button) then
		for _, framename in ipairs(useritem.TargetFrames) do
			local frameitem = self.Items[framename]

			if (frameitem) then
				local conditem = frameitem[useritem.Condition]

				if (conditem and conditem[useritem.Key]) then
					ErrorReport(conditem[useritem.Key], useritem, "Mouse button conflict")
					return false
				end
			end
		end

	--is keyboard key
	else
		--check keyboard key conflict
		for framename, frameitem in pairs(self.Items) do
			local conditem = frameitem[useritem.Condition]

			if (conditem and conditem[useritem.Key]) then
				ErrorReport(conditem[useritem.Key], useritem, "Key conflict")
				return false
			end
		end

		--init useritem.TargetFrames value
		if (not useritem.TargetFrames) then
			local targetframe = nil

			--find an exist one
			for _, framename in ipairs(ShortKeyButton.Items) do
				local frameitem = self.Items[framename]

				if (frameitem) then
					for _, conditem in pairs(frameitem) do
						if (conditem[useritem.Key]) then
							targetframe = framename
						end
					end
				end
			end

			--if not found, create one
			if (not targetframe) then
				targetframe = ShortKeyButton:Create()
			end

			useritem.TargetFrames = {targetframe}
		end
	end

	--check condition conflict
	for _, framename in ipairs(useritem.TargetFrames) do
		local frameitem = self.Items[framename]

		if (frameitem) then
			local index = table.getn(frameitem.ConditionSequence)

			if (frameitem.ConditionSequence[index] == NILCONDITION and useritem.Condition ~= NILCONDITION) then
				ErrorReport(nil, useritem, "Condition conflict")
				return false
			end
		end
	end

	return true
end


function InnerConfig:ItemImport(useritem)
	for _, framename in ipairs(useritem.TargetFrames) do
		local frameitem = self.Items[framename] or {}
		frameitem.ConditionSequence = frameitem.ConditionSequence or {}

		local conditem = frameitem[useritem.Condition]

		--dup condition only hold one place
		if (not conditem) then
			conditem = {}
			table.insert(frameitem.ConditionSequence, useritem.Condition)
		end

		conditem[useritem.Key] = useritem
		frameitem[useritem.Condition] = conditem

		self.Items[framename] = frameitem
	end
end

function InnerConfig:Import(userconfig)
	for _, useritem in ipairs(userconfig.Items) do
		if (userconfig:ItemInit(useritem) and self:ItemInit(useritem)) then
			self:ItemImport(useritem)
		end
	end
end

function InnerConfig:FillHeader(header)
	local index = 0

	for buttonname, _ in pairs(self.Items) do
		local button = GetButtonObject(buttonname)

		if (button) then
			index = index + 1
			header:SetFrameRef(buttonname, button)
			header:SetAttribute("button"..index, buttonname)
			self:FillButton(button)
		else
			ErrorReport(nil, nil, "Button not exist or not a Button or CheckButton type: "..buttonname)
		end
	end
end

function InnerConfig:FillButton(button)
	local frameitem = self.Items[button:GetName()]
	local needcond = (frameitem.ConditionSequence[1] ~= NILCONDITION)
	local conds = ""

	button:RegisterForClicks("AnyUp")
	button:SetAttribute("bakunit", button:GetAttribute("unit"))

	for i, condname in ipairs(frameitem.ConditionSequence) do
		--combine conditon
		if (needcond) then
			if (condname == NILCONDITION) then
				conds = conds.." "..i..";"
			else
				conds = conds..condname.." "..i..";"
			end
		end

		local perperfix = "cond"..i.."-"
		local count = 0

		for _, useritem in pairs(frameitem[condname]) do
			count = count + 1
			--format: cond[i]-[count]-[varname] = [varvalue]
			local perfix = perperfix..count.."-"

			button:SetAttribute(perfix.."type", useritem.Type)
			button:SetAttribute(perfix.."value", useritem.Value)
			button:SetAttribute(perfix.."target", useritem.Target)
			button:SetAttribute(perfix.."key", useritem.Key)
			button:SetAttribute(perfix.."modifier", useritem.Modifier)
			button:SetAttribute(perfix.."button", useritem.Button)
		end
	end

	if (needcond) then
		button:SetAttribute("conds", conds)
	end
end

------------------------------------------------------------------------------------------------------
--Eva
------------------------------------------------------------------------------------------------------

local ShortKeyAddon = EVA.Addon:New()
ShortKeyAddon.Name = "ShortKey"
ShortKeyAddon.Version = "4.6"
ShortKeyAddon.Description = "Binding shortcut key for your all action. By simonw. URL[http://cwowaddon.com/]"
EVA.AddonRegistry:Add(ShortKeyAddon)

local initBlock = {
	Name = "",
	Description = "",
	RequestTypes = {"Event"},
	Events = {"PLAYER_LOGIN"},
	Commands = {
		{Description = "", Function = ShortKey.Init},
	}
}

local settingBlock = {
	Name = "",
	Description = "",
	RequestTypes = {"SlashCommand"},
	SlashCommands = {"/shortkey", "/sk"},
	Commands = {
		{Description = "", Function = ShortKey.Setting},
	}
}

ShortKeyAddon:Add(EVA.Block:New(initBlock))
ShortKeyAddon:Add(EVA.Block:New(settingBlock))
--ShortKeyAddon:Add(EVA.Block:New(loaditemBlock))